package boardgamer.yavalath.common;

import boardgamer.common.annotations.Immutable;
import boardgamer.common.board.HexagonBoard;
import boardgamer.common.net.PlayerId;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.common.collect.Table.Cell;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Immutable
public class YavalathBoard extends HexagonBoard<YavalathTile> {

  private static final long serialVersionUID = 6026039989554971890L;

  private final int side;
  private final int loseLength;
  private final int winLength;

  private int unownedTilesCount;

  //  For serialization
  private YavalathBoard() {
    super();
    this.side = 1;
    this.loseLength = 1;
    this.winLength = 1;
  }

  public YavalathBoard(int side, int loseLength, int winLength) {
    super(new YavalathTileFactory(), side);
    this.side = side;
    this.loseLength = loseLength;
    this.winLength = winLength;
    unownedTilesCount = this.getTiles().size();
  }

  public void setToken(PlayerId owner, int x, int y) {
    YavalathTile tile = getTile(x, y);
    if (!tile.hasOwner()) {
      unownedTilesCount--;
    }
    tile.setOwner(owner);
  }

  public int getSide() {
    return side;
  }

  public int getUnownedTilesCount() {
    return unownedTilesCount;
  }

  public static YavalathBoard copyOf(YavalathBoard original) {
    YavalathBoard copy = new YavalathBoard(original.side, original.loseLength, original.winLength);
    for (Cell<Integer, Integer, YavalathTile> cell : original.board.cellSet()) {
      copy.board.put(cell.getRowKey(), cell.getColumnKey(),
          original.tileFactory.copyTile(cell.getValue()));
    }
    copy.unownedTilesCount = original.unownedTilesCount;
    return copy;
  }

  /**
   * Check if the given player places his token on the given location, he will
   * get four in a row. Assumes that player can always put the token at the
   * given location. If the given coordinate is invalid, returns null
   * <p/>
   * Returns null if the next move is not winning
   */
  @Nullable
  public List<YavalathTile> winningTiles(PlayerId player, int x, int y) {
    if (!hasTile(x, y)) {
      return null;
    }

    List<YavalathTile> xTiles = maxChain(player, x, y, 1, 0);
    xTiles.addAll(maxChain(player, x, y, -1, 0));
    List<YavalathTile> yTiles = maxChain(player, x, y, 0, 1);
    yTiles.addAll(maxChain(player, x, y, 0, -1));
    List<YavalathTile> zTiles = maxChain(player, x, y, 1, 1);
    zTiles.addAll(maxChain(player, x, y, -1, -1));

    List<YavalathTile> winningTiles = Lists.newArrayList();
    if (xTiles.size() >= winLength - 1) {
      winningTiles.addAll(xTiles);
    }

    if (yTiles.size() >= winLength - 1) {
      winningTiles.addAll(yTiles);
    }

    if (zTiles.size() >= winLength - 1) {
      winningTiles.addAll(zTiles);
    }

    if (winningTiles.isEmpty()) {
      return null;
    } else {
      winningTiles.add(new YavalathTile(player, x, y));
      return winningTiles;
    }
  }

  /**
   * Check if the given player places his token on the given location, he will
   * get three in a row. Assumes that player can always put the token at the
   * given location. If the given coordinate is invalid, returns null
   * <p/>
   * Returns null if the next move is not losing
   */
  @Nullable
  public List<YavalathTile> losingTiles(PlayerId player, int x, int y) {
    if (!hasTile(x, y)) {
      return null;
    }

    List<YavalathTile> xTiles = maxChain(player, x, y, 1, 0);
    xTiles.addAll(maxChain(player, x, y, -1, 0));
    List<YavalathTile> yTiles = maxChain(player, x, y, 0, 1);
    yTiles.addAll(maxChain(player, x, y, 0, -1));
    List<YavalathTile> zTiles = maxChain(player, x, y, 1, 1);
    zTiles.addAll(maxChain(player, x, y, -1, -1));

    List<YavalathTile> losingTiles = Lists.newArrayList();
    if (xTiles.size() >= loseLength - 1) {
      losingTiles.addAll(xTiles);
    }

    if (yTiles.size() >= loseLength - 1) {
      losingTiles.addAll(yTiles);
    }

    if (zTiles.size() >= loseLength - 1) {
      losingTiles.addAll(zTiles);
    }

    if (losingTiles.isEmpty()) {
      return null;
    } else {
      losingTiles.add(new YavalathTile(player, x, y));
      return losingTiles;
    }
  }

  /**
   * The current player has made a move at the given point. Return tiles that if the current player
   * takes those tiles, they will win
   */
  public Set<YavalathTile> nextWinningTiles(PlayerId player, int x, int y) {
    Set<YavalathTile> nextWinningTiles = Sets.newHashSet();

    for (int d = -(winLength - 1); d < winLength; d++) {
      addIfNextWinningTile(player, x + d, y, nextWinningTiles);
      addIfNextWinningTile(player, x, y + d, nextWinningTiles);
      addIfNextWinningTile(player, x + d, y + d, nextWinningTiles);
    }

    return ImmutableSet.copyOf(nextWinningTiles);
  }

  private void addIfNextWinningTile(PlayerId player, int x, int y,
      Set<YavalathTile> nextWinningTiles) {
    YavalathTile tile = findTile(x, y);
    if (tile != null && !tile.hasOwner() && winningTiles(player, x, y) != null) {
      nextWinningTiles.add(new YavalathTile(x, y));
    }
  }

  /**
   * Returns the maximum length of chain around (poxY, posY) in direction
   * (dirX, dirY)
   */
  @VisibleForTesting
  ArrayList<YavalathTile> maxChain(PlayerId player, int posX, int posY, int dirX, int dirY) {
    ArrayList<YavalathTile> tiles = Lists.newArrayList();
    for (int step = 1; hasTile(posX + step * dirX, posY + step * dirY); step++) {
      YavalathTile tile = getTile(posX + step * dirX, posY + step * dirY);
      if (tile.hasOwner() && tile.getOwner().equals(player)) {
        tiles.add(tile);
      } else {
        break;
      }
    }
    return tiles;
  }

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof YavalathBoard)) {
      return false;
    }

    YavalathBoard that = (YavalathBoard) obj;
    return Objects.equal(this.board, that.board)
        && Objects.equal(this.side, that.side)
        && Objects.equal(this.loseLength, that.loseLength)
        && Objects.equal(this.winLength, that.winLength);
  }

  @Override
  public int hashCode() {
    return Objects.hashCode(board, side, loseLength, winLength);
  }

  @Override
  public String toString() {
    return Objects.toStringHelper(this)
        .add("board", board)
        .add("side", side)
        .toString();
  }

  private static class YavalathTileFactory implements TileFactory<YavalathTile> {

    @Override
    public YavalathTile createTile(int x, int y) {
      return new YavalathTile(x, y);
    }

    @Override
    public YavalathTile copyTile(YavalathTile tile) {
      return YavalathTile.copyOf(tile);
    }
  }
}
